Só há um problema: estamos falando da função main() que usa uma variável que recebe um valor inteiro. Se vocẽ quiser usar essa execve que conta os parâmetros ao invés, aí é outra história ... :-P A exemplo do último parâmetro de main() pode ser acessada com uma função sem usar o parâmetro. Falei sobre isso lá atrás.
OBS: Faltou explicar a função assert() :-P
Hein?
A chamada execve() não é uma alternativa a main(), mas sim a chamada que faz com que o kernel execute um programa novo.
Por exemplo, quando você digita no shell um comando como
expr 23 + 45
o que o shell vai fazer é contar quantos são os componentes da linha de comando (4 strings, que no nosso exemplo são "expr", "23", "+" e "45"), alocar espaço na memória para 5 (i.e. 4+1) ponteiros de caracteres, aos quais ele vai atribuir os endereços das 4 strings e, no fim, um ponteiro nulo), descobrir um qual diretório do PATH reside o comando "expr", e executar esse programa. Em algum momento do tempo, ter-se-á executado alguma coisa funcionalmente equivalente a tudo o que vai abaixo.
extern char **environ; /* array com as variáveis de ambiente do processo */
char *executavel, **argumentos;
int nargs;
Admitindo que o comando expr tenha sido escrito em C (e realmente foi!), sua função main() terá sido escrita na forma int main(int argc, char **argv). Quando ele for disparado pelo execve() mostrado acima, argc terá o valor 4, indicando que há em argv quatro elementos aproveitáveis (índices 0 a argc-1, que é 3, do array, portanto).
Eu digo, porém, que o tamanho real do array argv não é 4, mas 5, porque haverá (ao menos no Linux) um ponteiro nulo lodo após o último elemento "aproveitável". Esse ponteiro nulo poderia ser usado para descobrir o final da lista de argumentos sem consultar o valor de argc -- ao contrário do que você sugeriu ao dizer que usar argc é a única forma de percorrer argv.
Quanto a assert(), a manpage lhe dirá que é uma função que aborta o programa caso a condição testada seja falsa. Eu a usei no exemplo que mostrei só para deixar claro que o programa não será abortado se acaso resolver verificar se argv[argc] é realmente nulo ou não.
Mencionei assert() porque pareceu-me tão contextualizada quanto execve(). Mas você tem razão: relendo agora vi que confundi o número de parâmetros com o tamanho do vetor.